Android ClassLoader流程解读并简单方式实现热更新

ClassLoader在启动Activity的时候会调用loadClass方法,我们就从这里入手:

1
2
3
4
5
6
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}

然后我们点击进入进入了ClassLoader的loadClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
}
}
return c;
}

看到源码是调用了findClass方法:

1
2
3
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

发现这个是抛出了一个异常,就没办法继续阅读了。这个时候发现ClassLoader是一个抽象类,应该是子类重写了这个方法,然后通过启动StartActivity的源码可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
String librarySearchPath, String libraryPermittedPath,
ClassLoader parent) {
/*
* This is the parent we use if they pass "null" in. In theory
* this should be the "system" class loader; in practice we
* don't use that and can happily (and more efficiently) use the
* bootstrap class loader.
*/
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}

/*
* If we're one step up from the base class loader, find
* something in our cache. Otherwise, we create a whole
* new ClassLoader for the zip archive.
*/
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);

PathClassLoader pathClassloader = PathClassLoaderFactory.createClassLoader(
zip,
librarySearchPath,
libraryPermittedPath,
parent,
targetSdkVersion,
isBundled);

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "setupVulkanLayerPath");
setupVulkanLayerPath(pathClassloader, librarySearchPath);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

mLoaders.put(zip, pathClassloader);
return pathClassloader;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return pathClassloader;
}
}

得知这个ClassLoaderPathClassLoader,点进去查看

1
2
3
4
5
6
7
8
9
10
11
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super((String)null, (File)null, (String)null, (ClassLoader)null);
throw new RuntimeException("Stub!");
}
}

发现其实是BaseDexClassLoader里面的findClass()生效了,我们通过http://androidxref.com/ 查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private final DexPathList pathList;

/**
* Constructs an instance.
*
* @param dexPath the list of jar/apk files containing classes and
* resources, delimited by {@code File.pathSeparator}, which
* defaults to {@code ":"} on Android
* @param optimizedDirectory directory where optimized dex files
* should be written; may be {@code null}
* @param libraryPath the list of directories containing native
* libraries, delimited by {@code File.pathSeparator}; may be
* {@code null}
* @param parent the parent class loader
*/


public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

}


@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {

ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);

}
throw cnfe;
}
return c;
}

其实就是DexPathList pathList这个字段得到Class<?>,我们继续查看DexPathList里面源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private final Element[] dexElements;


/**
* * Finds the named class in one of the dex files pointed at by
* * this instance. This will find the one in the earliest listed
* * path element. If the class is found but has not yet been
* * defined, then this method will define it in the defining
* * context that this instance was constructed with.
* *
* * @param name of class to find
* * @param suppressed exceptions encountered whilst finding the class
* * @return the named class or {@code null} if the class is not
* * found in any of the dex files
*
*/
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;

if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

可以发现其实是一个dexElement[]数组,通过for循环得到相应的值,所以我们就可以把改变的class打成dex格式的文件,通过反射把这个dex文件里面的Element[] dexElements值插入到原APP的Element[] dexElements前就可以了。
下面是具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package com.zzw.baselibray.fixBug;

import android.content.Context;
import android.util.Log;

import com.zzw.baselibray.util.FileUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import dalvik.system.BaseDexClassLoader;

/**
* Created by zzw on 2017/5/5.
* 热修复管理
*/

public class FixBugManager {

private static final String TAG = "FixBugManager";

private Context mContext;
private File mDexDir;//应用可以访问的dex目录

public FixBugManager(Context context) {
this.mContext = context;
//获取到应用可以访问的dex目录
this.mDexDir = context.getDir("odex", Context.MODE_PRIVATE);
}


/**
* 设置新的dexElements到applicationClassLoader里面
*
* @param classLoader
* @param dexElements
*/
private void setElementsToClassLoader(ClassLoader classLoader, Object dexElements) throws NoSuchFieldException, IllegalAccessException {
//1.先获取ClassLoader里面的pathList
Field pathListFiled = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathList = pathListFiled.get(classLoader);

//2.获取pathList里面的dexElements字段并设置新的值
Field dexElementsField = pathList.getClass().getField("dexElements");
dexElementsField.setAccessible(true);
dexElementsField.set(pathList, dexElements);
}

/**
* 从ClassLoader里面获取dexElements
*
* @param applicationClassLoader
* @return
*/
private Object getElementsByClassLoader(ClassLoader applicationClassLoader) throws NoSuchFieldException, IllegalAccessException {
//1.先获取ClassLoader里面的pathList
Field pathListFiled = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathList = pathListFiled.get(applicationClassLoader);

//2.获取pathList里面的dexElements
Field dexElementsField = pathList.getClass().getField("dexElements");
dexElementsField.setAccessible(true);
Object dexElements = dexElementsField.get(pathList);

return dexElements;
}


/**
* 合并两个dexElements数组
*
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}

/**
* 加载所有的修复包
*/
public void loadFixDex() throws Exception {
File[] files = mDexDir.listFiles();
List<File> fixDexFiles = new ArrayList<>();
for (File dexFile : files) {
if (dexFile.getName().endsWith(".dex")) {
fixDexFiles.add(dexFile);
}
}
fixDexFiles(fixDexFiles);
}

/**
* 修复dex包
*
* @param fixDexPath dexDex修复路径
*/
public void fixDex(String fixDexPath) throws Exception {

File srcFile = new File(fixDexPath);
if (!srcFile.exists()) {
throw new FileNotFoundException(fixDexPath);
}
File destFile = new File(mDexDir, srcFile.getName());
if (destFile.exists()) {
Log.d(TAG, "patch [" + fixDexPath + "] has be loaded.");
return;
}
FileUtil.copyFile(srcFile, destFile);// copy to patch's directory
// FileUtil.deleteFile(srcFile);//copy完成后删除
//2.2 ClassLoader读取fixDex路径 为什么加入到集合?-->可能已启动就可能要修复
List<File> fixDexFiles = new ArrayList<>();
fixDexFiles.add(destFile);

fixDexFiles(fixDexFiles);
}

/**
* 修复dex 已经修复过的dex文件全部copy在mContext里面,application初始化的时候将这些多个dex文件一起修复
*
* @param fixDexFiles
*/
private void fixDexFiles(List<File> fixDexFiles) throws Exception {
//1.先获取applicationClassLoader的pathList字段的dexElements值
ClassLoader applicationClassLoader = mContext.getClassLoader();
Object applicationDexElements = getElementsByClassLoader(applicationClassLoader);

//2.获取下载好的补丁的dexElements
//2.1 移动到系统能够访问的dex目录下 --> ClassLoader
File optimizedDirectory = new File(mDexDir, "oder");
if (!optimizedDirectory.exists())
optimizedDirectory.mkdirs();
//修复
for (File fixDexFile : fixDexFiles) {
//dexPath 加载的dex路径
//optimizedDirectory 解压路径
//librarySearchPath so文件位置
//parent 父ClassLoader
ClassLoader fixClassLoader = new BaseDexClassLoader(
fixDexFile.getAbsolutePath(), //dexPath 加载的dex路径
optimizedDirectory,// 解压文件
null,
applicationClassLoader);
Object fixDexElements = getElementsByClassLoader(fixClassLoader);

//3.把补丁的dexElements插到已经已经运行的dexElements前面
//合并完成 fixDexElements插入dexElements之前
applicationDexElements = combineArray(fixDexElements, applicationDexElements);
//把合并的数组注入到原来的applicationClassLoader类中
setElementsToClassLoader(applicationClassLoader, applicationDexElements);
}
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/*
*
* Copyright (c) 2015, alipay.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.zzw.baselibray.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

/**
* file utility
*
* @author sanping.li@alipay.com
*/
public class FileUtil {

/**
* copy file
*
* @param src source file
* @param dest target file
* @throws IOException
*/
public static void copyFile(File src, File dest) throws IOException {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
if (!dest.exists()) {
dest.createNewFile();
}
inChannel = new FileInputStream(src).getChannel();
outChannel = new FileOutputStream(dest).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
} finally {
if (inChannel != null) {
inChannel.close();
}
if (outChannel != null) {
outChannel.close();
}
}
}

/**
* delete file
*
* @param file file
* @return true if delete success
*/
public static boolean deleteFile(File file) {
if (!file.exists()) {
return true;
}
if (file.isDirectory()) {
File[] files = file.listFiles();
for (File f : files) {
deleteFile(f);
}
}
return file.delete();
}
}

学习来源:红橙Darren

-------------The End-------------